Tema 02 - Limpieza y Tratamiento de Datos

De datos en bruto a información útil para decidir con tidyverse

Pedro Albarrán

Dpto. de Fundamentos del Análisis Económico. Universidad de Alicante

Alberto Pérez

Introducción. Datos Ordenados

Limpieza y “doma” de datos

  • El ciclo de vida de los datos

  • El desafío real del análisis de datos:

    • 80% del tiempo de trabajo “sucio” : limpieza y preparación

    • 20% del tiempo: análisis y modelización

  • tidyverse incluye una colección de bibliotecas con herramientes eficientes para el proceso de “tratamiento de datos” (“data wrangling”)

  • El objetivo es obtener un conjunto de datos ordenado y limpio para realizar el análisis eficientemente y obtener información útil para la toma de decisiones

Datos ordenados (‘tidy data’): Principios fundamentales

1.- Cada columna es una variable: mide el mismo atributo entre unidades

2.- Cada fila es una observación (caso): misma unidad a través de atributos

3.- Cada celda es un valor

  • Tenemos información similar y no redundante en una misma tabla

  • Es una forma natural (variable = vector columna) para trabajar con datos

  • tidyverse es eficiente con datos ordenados

Datos no ordenados

  • Otras estructuras como esta pueden tener sentido para mostrar información (o por convenciones)

  • La visualización es atractiva, PERO sobran filas para analizar los datos: ej., total de personas con hijos y sin pareja entre 30 y 39 años

Caso de Estudio: RetailCorp

  • Contexto: Cadena de venta al por menor scon 12 tiendas en España

  • Objetivo: Analizar rendimiento de ventas para toma de decisiones estratégicas

load("data/retail_data.RData")
  • Recibe datos de

    • Sistema de ventas (POS)

    • CRM de clientes

    • Inventario de productos

    • RRHH de empleados

    • Informes Excel de gerentes

Problemas Típicos:

  • Formatos inconsistentes

  • Datos duplicados

  • Valores ausentes

  • Estructuras inadecuadas

  • Tablas dispersas

Transformación de datos (una tabla)

Funciones de transformación de datos

  • La mayoría de operaciones pueden realizarse combinando 5 “verbos”:

    1. select(): selecciona columnas (variables)

    2. filter(): filtra (extraer) filas

    3. mutate(): crea nuevas columnas

    4. arrange(): ordena filas

    5. summarize(): crea resúmenes de la tabla

    • Más la tubería %>% o |>

    • y group_by()

  • NOTA: existe una colección de “chuletas” de R, p.e., para transformación.

  • Todos tienen como primer argumento un data frame, los siguientes describen qué hacer (con columnas o filas) y devuelven otro data frame

1. select()

  • Selecciona variables por nombres o posiciones de columnas, separados por comas
  • Ej., un analista solo necesita información básica de ventas
select(ventas, id_venta, fecha, id_tienda, id_producto, total)

select(ventas, 1:2, 5, 4, 12)
  • Aplicación Empresarial: el equipo de marketing solo necesita información de cliente y venta
ventas_mkt <- select(ventas, 
                fecha, id_cliente, id_producto, total)

2. filter()

  • Conserva filas en las que una condición lógica (o varias separadas por comas) es verdadera
  • Caso de Uso: Gerente quiere analizar ventas específicas con determinadas características
ventas_top      <- filter(ventas, total > 100)

ventas_ene_2024 <- filter(ventas, año == 2024, mes == 1)

ventas_mad_bcn  <- filter(ventas, id_tienda %in% c(1, 2))

ventas_premium  <- filter(ventas, 
                     total > 100 & descuento_porcentaje == 0)

Encadenando operaciones con tuberías: %>% o |>

  • Las operaciones encadenadas no son legibles o crean objetos intermedios
ventas_top_markt <- select(filter(ventas, total > 100),
                      fecha, id_cliente, id_producto, total)

ventas_top        <- filter(ventas, total > 100)
ventas_top_markt  <- select(ventas_top, 
                       fecha, id_cliente, id_producto, total)
  • datos %>% filter(condicion) equivale a filter(datos, condicion)
  • El anidamiento con tuberías sigue el flujo natural de lectura

    • Toma una tabla y pásala a un comando que acepta y produce un data frame
    • Toma la nueva tabla resultante y pásala a otro comando
ventas_top_markt <- ventas |>                   
                     filter(total > 100) |>     
                     select(fecha, id_cliente, id_producto, total)

Funciones auxiliares de selección (de columnas)

# Por rango de columnas
ventas |> select(id_venta:id_tienda)

# Excluir columnas
ventas |> select(-descuento_porcentaje, -descuento_aplicado)

# Por patrón de nombre
ventas |> select(starts_with("id_"))
ventas |> select(ends_with("_porcentaje"))
ventas |> select(contains("descuento"))

# Por tipo de dato
ventas |> select(where(is.numeric))
ventas |> select(where(is.character))
  • pull(): extrae una única columna, como vector
ventas |> pull(cantidad) |> mean()

3. mutate()

  • Crea o modifica variables mediante una fórmula a partir de otras columnas

ventas2 <- ventas |>
  mutate(
    precio_final_unitario = total / cantidad,
    es_inicio_mes         = day(fecha) <= 7
  )
  • Funciones para operar con fechas (usando lubridate)
ventas_tiempo <- ventas |>
  mutate(
    fecha_completa = as.Date(fecha),  # tipo de objeto "fecha"
    semana_año = week(fecha),
    nombre_mes = month(fecha, label = TRUE),
    dias_desde_venta = as.numeric(Sys.Date() - fecha)
  )

4. arrange()

  • re-ordena las filas todas las columnas de un data frame
    • en orden ascendente (por defecto) o descendente con desc()

  • Caso de Uso: Top 10 ventas más altas
ventas |> 
  arrange(desc(total)) |> 
  select(id_venta, fecha, total) |> head(10) 
  • Ordenamientos múltiples: ordena por la primera variable y luego, en caso de empate, por la siguiente, etc.
ventas |>
  arrange(id_tienda, desc(total)) |>
  select(id_tienda, id_venta, total) |> head(10)

5. summarize()

  • Crea un nuevo conjunto de datos de una sola fila, con variables nuevas de un solo valor que resumen los datos completos

  • Caso de Uso: KPIs para el dashboard ejecutivo
ventas |>
  summarize(
    total_ventas       = n(),           # Volumen (núm. de filas)
    ingresos_totales   = sum(total),    # Ingresos
    ingresos_promedio  = mean(total),
    ingresos_mediano   = median(total),
    descuento_promedio = mean(descuento_porcentaje), # Descuentos
    descuento_total    = sum(descuento_aplicado),
    unidades_vendidas  = sum(cantidad),             # Productos
    clientes_unicos    = n_distinct(id_cliente)     # Clientes 
                         # (núm. de filas distintas)
  )

group_by(): Análisis por Grupos

  • Cambia el alcance de cada función para que no actúe sobre todo el data frame sino en grupos individuales
  • group_by() + summarize() = el poder de la agregación

    • Concepto: cambiar el nivel de análisis: de transacciones a tiendas, productos, etc.)

    • En Excel: Tablas dinámicas, AGRUPARPOR() (y SUMAR.SI/SUMIF)

ventas                             # tabla a nivel de transacción

ventas |>
  summarize(ingresos = sum(total)) # resumen global

ventas |>
  group_by(id_tienda) |>
  summarize(ingresos = sum(total)) # resumen por tienda
                                   # tabla a nivel de tienda

Combinando mutate() con group_by()

  • Diferencia Clave:

    • group_by() + summarize(): Reduce filas (nuevo dataset agregado a nivel de los grupos)

    • group_by() + mutate(): Mantiene filas (añade columnas calculadas por grupo a nivel de la tabla original)

  • Ejemplo: Porcentaje de las ventas mensuales que representa cada transacción

ventas |>
  group_by(id_tienda, mes) |>
  mutate(
    ventas_tienda_mes = sum(total),
    pct_de_ventas_mes = total / ventas_tienda_mes * 100
  ) |>
  slice_max(pct_de_ventas_mes) |> 
  select(id_tienda, mes, id_venta, total, pct_de_ventas_mes)

Análisis multinivel y ungroup()

  • Encontrar la fecha con la mayor venta por tienda
ventas |>
  group_by(id_tienda) |>
  arrange(desc(total)) |>  
  slice(1) |>                     # Primera fila (de cada grupo)
  select(id_tienda, fecha, total)
# alternativa: slice_max()
  • IMPORTANTE: No olvidar ungroup() o .groups = "drop" después de terminar operaciones agrupadas
  • Cálculo de Porcentajes Globales: sin desagrupar, sum(total) suma por tienda → siempre da 100%
ventas |>
  group_by(id_tienda) |>
  mutate(ventas_tienda = sum(total)) |>
  ungroup() |>
  mutate(porcentaje = ventas_tienda / sum(total) * 100)

Análisis multinivel y ungroup() (cont.)

  • Filtrar Top Global: sin desagrupar, será top 5 de cada tienda
ventas |>
  group_by(id_tienda) |>
  mutate(ventas_tienda = sum(total), 
         .groups = "drop" ) |>
  arrange(desc(ventas_tienda)) |>
  slice_head(n = 5)
  • Media Global después de agrupar: sin desagrupar, 1 fila por tienda (media mensual de cada tienda)
# Ventas por tienda y mes
ventas_mes_por_tienda <- ventas |>
  group_by(id_tienda, mes) |>
  summarize(total_mes = sum(total))

ventas_mensuales_medias <- ventas_mes_por_tienda |>
  ungroup() |>
  summarize(media = mean(total_mes))
# Resultado: 1 fila (media de todos los meses de todas las tiendas)

Funciones auxiliares para filas

  • Extraer filas pero NO por condición: por posición (slice(), slice_head()), aleatoriamente (slice_sample()), etc.
ventas |> slice_max(total, n = 5) # Top 5 ventas
ventas |> slice_sample(n = 100)   # sub-muestra aleatoria
  • distinct(): extrae sólo las filas únicas (una o varias variables)
ventas %>% distinct(id_producto)
  • drop_na() y replace_na(): elimina/reemplaza filas con valores ausentes
# Quita filas con NA en cualquier variable
ventas_completas <- ventas %>%
  drop_na()                    

# solo quita si precio_unitario es NA
ventas_completas_precio <- ventas %>%
  drop_na(precio_unitario)  

Otras funciones auxilidares de tidyverse

  • Muchas funciones son equivalentes a otras de R base:

    • parse_number(), parse_factor(), etc. por as.numeric(), as.factor(), etc.

    • bind_cols() y bind_rows() por cbind() y rbind()

    • if_else() y case_when() para ejecución condicional (ifelse())

ventas |>
  mutate(
    tipo_venta = if_else(total > 100, "Alta", "Baja"), # condición simple
    categoria_cliente = case_when(              # múltiples condiciones
      total < 30  ~ "Económico",
      total < 100 ~ "Estándar",
      TRUE        ~ "Premium"               # OJO: convertir a factor
    )
  )
  • Discretizar variables: cut_interval(), cut_number(), cut_width()
  • Funciones para fechas de lubridate: year(), month(), `day(), quarter(), week()

Funciones auxiliares de creación de variables

  • rename(): cambiar el nombre de una columna
ventas_renamed <- ventas |>
  rename(
    fecha_venta = fecha,
    monto_total = total
  )
  • across(): aplica la misma transformación a múltiples columnas
ventas |> mutate(across(c(cantidad, subtotal:total), ~ log(.x)))
ventas |> mutate(across(where(is.character), ~ parse_factor(.x)))
  • Operadores aritméticos (+, -, *, /, ^, %/%, %%) y lógicos (<, <=, >, >=, !=)

  • Funciones como log(), lag(), lead(), cumsum(), row_number() etc.

Funciones auxiliares de resumen

  • count(): cuenta los valores únicos de una o más variables
ventas |> count(id_tienda)
# ventas |> group_by(id_tienda) |>  summarize(n = n())
ventas |> count(id_tienda, sort = TRUE)
  • Medidas de centralidad y de dispersión: mean(x), median(x), sd(x), IQR(x)

  • Medidas de rango: min(x), quantile(x, 0.25), max(x)

  • Medidas de posición: first(x), nth(x, 2), last(x).

  • Sumas, productos, etc.

  • Conteos:

    • n(): observaciones totales (tamaño del grupo)

    • n_distinct(x): filas distintas en x

Transformación de Datos: Pivotar

::::

Mismos datos, dos formatos: ancho o largo

  • La utilidad de almacenar los datos en un rectángulo ancho (“wide”) o en uno largo (“long”) depende de qué queramos hacer

    • P.e., Excel prefiere el formato largo para tablas dinámicas, fórmulas de agregación (SUMAR.SI) y algunos gráficos

  • El cambio de forma entre formatos es una tarea habitual del analista de datos.

  • Cambiar entre representación larga y ancha se conoce como pivotar (o girar)

table4a        # formato ancho
table1         # formato largo

El Problema del Formato “Ancho”

  • Situación Real: Reporte mensual enviado por finanzas
ventas_ancho <- tibble(
  tienda = c("Madrid", "Barcelona", "Valencia"),
  Ene_2023 = c(45000, 52000, 38000),
  Feb_2023 = c(48000, 54000, 41000),
  Mar_2023 = c(51000, 56000, 39000),
  Abr_2023 = c(49000, 55000, 42000)
)
  • Problemas:
    • No se puede filtrar por mes fácilmente

    • No se puede hacer group_by por mes

    • Difícil calcular tendencias temporales

    • No es “tidy data”

pivot_longer(): De Ancho a Largo

# Convertir a formato largo (ordenado)
ventas_largo <- ventas_ancho %>%
  pivot_longer(
    cols = Ene_2023:Abr_2023,        # columnas a pivotar
    names_to = "mes_año",             # nombre para los encabezados
    values_to = "ingresos"            # nombre para los valores
  )

ventas_largo

Ahora sí podemos analizar:

# Separar mes y año
ventas_largo_limpio <- ventas_largo %>%
  separate(mes_año, into = c("mes", "año"), sep = "_") %>%
  mutate(año = as.numeric(año))

# Análisis temporal por tienda
ventas_largo_limpio %>%
  group_by(tienda) %>%
  mutate(
    crecimiento = ingresos - lag(ingresos),
    pct_crecimiento = round((ingresos - lag(ingresos)) / lag(ingresos) * 100, 1)
  )

# Visualización fácil
library(ggplot2)
ggplot(ventas_largo_limpio, aes(x = mes, y = ingresos, color = tienda, group = tienda)) +
  geom_line() +
  geom_point() +
  theme_minimal() +
  labs(title = "Evolución de Ingresos por Tienda")

Caso Real: Datos de Ventas Mensuales

# Crear reporte mensual en formato ancho
reporte_mensual_ancho <- ventas %>%
  mutate(mes_nombre = paste(mes, año, sep = "_")) %>%
  group_by(id_tienda, mes_nombre) %>%
  summarize(ingresos = sum(total), .groups = "drop") %>%
  pivot_wider(
    names_from = mes_nombre,
    values_from = ingresos,
    values_fill = 0  # llenar con 0 si no hay ventas
  )

head(reporte_mensual_ancho)

# Volver a formato largo para análisis
reporte_mensual_largo <- reporte_mensual_ancho %>%
  pivot_longer(
    cols = -id_tienda,
    names_to = "periodo",
    values_to = "ingresos"
  ) %>%
  separate(periodo, into = c("mes", "año"), sep = "_", convert = TRUE)

reporte_mensual_largo

pivot_wider(): De Largo a Ancho

Caso de Uso: Crear tabla de presentación para ejecutivos

# Tabla comparativa: ingresos por región y trimestre
tabla_ejecutiva <- ventas %>%
  left_join(tiendas %>% select(id_tienda, region), by = "id_tienda") %>%
  filter(año == 2023) %>%
  group_by(region, trimestre) %>%
  summarize(ingresos = sum(total), .groups = "drop") %>%
  pivot_wider(
    names_from = trimestre,
    values_from = ingresos,
    names_prefix = "Q"
  ) %>%
  mutate(
    Total = Q1 + Q2 + Q3 + Q4,
    across(Q1:Q4, ~round(.x / 1000, 1))  # en miles
  )

tabla_ejecutiva

Separar y Unir Columnas

# separate(): dividir columna en múltiples
datos_ejemplo <- tibble(
  codigo = c("MAD-001", "BCN-002", "VAL-003"),
  fecha_hora = c("2023-01-15 10:30", "2023-01-16 14:20", "2023-01-17 09:15")
)

datos_separados <- datos_ejemplo %>%
  separate(codigo, into = c("ciudad", "numero"), sep = "-") %>%
  separate(fecha_hora, into = c("fecha", "hora"), sep = " ")

datos_separados

# unite(): combinar columnas
datos_unidos <- datos_separados %>%
  unite("codigo_completo", ciudad, numero, sep = "-") %>%
  unite("timestamp", fecha, hora, sep = " ")

datos_unidos

Datos Relacionales

Por Qué Múltiples Tablas

Ventajas del Diseño Relacional:

  1. Eficiencia: No repetir información
  2. Consistencia: Un solo lugar para actualizar
  3. Integridad: Relaciones claras entre datos
  4. Escalabilidad: Fácil añadir nuevas tablas
# Ejemplo: información duplicada vs relacional

# MAL: Todo en una tabla (información repetida)
ventas_todo_junto <- ventas %>%
  left_join(productos, by = "id_producto") %>%
  left_join(tiendas, by = "id_tienda") %>%
  left_join(clientes, by = "id_cliente")

# BIEN: Tablas separadas, unir solo cuando necesario
# ventas, productos, tiendas, clientes son tablas independientes

Tipos de Uniones: Visión General

# Datos de ejemplo para ilustrar
df1 <- tibble(id = 1:3, valor_x = c("A", "B", "C"))
df2 <- tibble(id = c(1, 2, 4), valor_y = c("X", "Y", "Z"))

# inner_join: solo coincidencias
inner_join(df1, df2, by = "id")  # resultado: 1, 2

# left_join: todas de la izquierda
left_join(df1, df2, by = "id")   # resultado: 1, 2, 3 (3 con NA)

# right_join: todas de la derecha
right_join(df1, df2, by = "id")  # resultado: 1, 2, 4 (4 con NA)

# full_join: todas de ambas
full_join(df1, df2, by = "id")   # resultado: 1, 2, 3, 4

Caso Real: Enriquecer Datos de Ventas

Objetivo: Análisis completo con información de todas las tablas

# Crear dataset completo para análisis
ventas_completo <- ventas %>%
  # Información de productos
  left_join(productos %>% select(id_producto, nombre_producto, id_categoria, precio, costo),
            by = "id_producto") %>%
  
  # Información de categorías
  left_join(categorias, by = "id_categoria") %>%
  
  # Información de tiendas
  left_join(tiendas %>% select(id_tienda, nombre_tienda, region, tamaño_m2),
            by = "id_tienda") %>%
  
  # Información de clientes
  left_join(clientes %>% select(id_cliente, programa_fidelidad, fecha_registro),
            by = "id_cliente") %>%
  
  # Información de empleados
  left_join(empleados %>% select(id_empleado, puesto),
            by = "id_empleado")

# Ahora podemos hacer análisis complejos
glimpse(ventas_completo)

Análisis Enriquecido:

# Rentabilidad por categoría y región
analisis_rentabilidad <- ventas_completo %>%
  mutate(
    margen_venta = precio - costo,
    rentabilidad = margen_venta * cantidad
  ) %>%
  group_by(nombre_categoria, region) %>%
  summarize(
    num_ventas = n(),
    ingresos = sum(total),
    rentabilidad_total = sum(rentabilidad, na.rm = TRUE),
    margen_porcentaje = round(mean(margen_venta / precio * 100, na.rm = TRUE), 1),
    .groups = "drop"
  ) %>%
  arrange(desc(rentabilidad_total))

head(analisis_rentabilidad, 10)

Caso de Uso: Performance por región y mes

# Primero unir con info de tiendas para obtener región
performance_regional <- ventas %>%
  left_join(tiendas %>% select(id_tienda, region), by = "id_tienda") %>%
  group_by(region, año, mes) %>%
  summarize(
    ventas_totales = n(),
    ingresos = sum(total),
    ticket_promedio = mean(total),
    .groups = "drop"
  ) %>%
  arrange(region, año, mes)

head(performance_regional, 10)

Caso de Uso: Performance por región y mes

# Primero unir con info de tiendas para obtener región
performance_regional <- ventas %>%
  left_join(tiendas %>% select(id_tienda, region), by = "id_tienda") %>%
  group_by(region, año, mes) %>%
  summarize(
    ventas_totales = n(),
    ingresos = sum(total),
    ticket_promedio = mean(total),
    .groups = "drop"
  ) %>%
  arrange(region, año, mes)

head(performance_regional, 10)
  • Aplicación: Crecimiento anual
# Comparación mensual por región
crecimiento_mensual <- performance_regional %>%
  group_by(region, mes) %>%
  arrange(año) %>%
  mutate(
    ingresos_año_anterior = lag(ingresos),
    crecimiento_absoluto = ingresos - ingresos_año_anterior,
    crecimiento_porcentual = round(
      (ingresos - ingresos_año_anterior) / ingresos_año_anterior * 100, 1
    )
  ) %>%
  filter(!is.na(crecimiento_porcentual))

# Regiones con mayor crecimiento
crecimiento_mensual %>%
  filter(año == 2023) %>%
  group_by(region) %>%
  summarize(crecimiento_promedio = mean(crecimiento_porcentual, na.rm = TRUE)) %>%
  arrange(desc(crecimiento_promedio))

Uniones de Filtrado

semi_join: Mantener filas que tienen coincidencia

# Clientes que SÍ han comprado
clientes_activos <- clientes %>%
  semi_join(ventas, by = "id_cliente")

nrow(clientes_activos)  # ¿Cuántos clientes activos?

# Productos que SÍ se han vendido
productos_vendidos <- productos %>%
  semi_join(ventas, by = "id_producto")

anti_join: Mantener filas que NO tienen coincidencia

# Clientes registrados pero sin compras
clientes_sin_compras <- clientes %>%
  anti_join(ventas, by = "id_cliente")

nrow(clientes_sin_compras)  # Oportunidad de marketing

# Productos en catálogo pero nunca vendidos
productos_sin_vender <- productos %>%
  anti_join(ventas, by = "id_producto") %>%
  filter(activo == TRUE)  # y que estén activos

nrow(productos_sin_vender)  # ¿Eliminar del catálogo?

Uniones con Claves Múltiples

# Crear tabla de objetivos mensuales por tienda
objetivos <- tibble(
  id_tienda = rep(1:12, each = 12),
  mes = rep(1:12, times = 12),
  objetivo_ingresos = runif(144, 30000, 80000)
)

# Comparar ventas reales vs objetivos
comparacion_objetivos <- ventas %>%
  group_by(id_tienda, mes) %>%
  summarize(ingresos_reales = sum(total), .groups = "drop") %>%
  left_join(objetivos, by = c("id_tienda", "mes")) %>%
  mutate(
    diferencia = ingresos_reales - objetivo_ingresos,
    cumplimiento_pct = round(ingresos_reales / objetivo_ingresos * 100, 1),
    cumple_objetivo = cumplimiento_pct >= 100
  )

# Tiendas que más superan objetivos
comparacion_objetivos %>%
  filter(cumple_objetivo) %>%
  group_by(id_tienda) %>%
  summarize(
    meses_cumplidos = n(),
    exceso_promedio = mean(diferencia)
  ) %>%
  arrange(desc(meses_cumplidos))

Caso de Estudio Completo: Análisis de Devoluciones

Problema de Negocio

Contexto: Gerente de operaciones preocupado por devoluciones

Preguntas: 1. ¿Qué productos tienen más devoluciones? 2. ¿Hay patrones por tienda o categoría? 3. ¿Impacto financiero de las devoluciones? 4. ¿Recomendaciones accionables?

Paso 1: Exploración Inicial

# Visión general de devoluciones
glimpse(devoluciones)

# Estadísticas básicas
devoluciones %>%
  summarize(
    total_devoluciones = n(),
    tasa_devolucion = n() / nrow(ventas) * 100,
    reembolso_total = sum(reembolso),
    reembolso_promedio = mean(reembolso),
    dias_promedio = mean(as.numeric(fecha_devolucion - 
                        ventas$fecha[match(id_venta, ventas$id_venta)]), na.rm = TRUE)
  )

Paso 2: Análisis por Motivo

# Distribución de motivos
analisis_motivos <- devoluciones %>%
  group_by(motivo) %>%
  summarize(
    num_devoluciones = n(),
    reembolso_total = sum(reembolso),
    .groups = "drop"
  ) %>%
  mutate(
    pct_devoluciones = round(num_devoluciones / sum(num_devoluciones) * 100, 1),
    pct_reembolso = round(reembolso_total / sum(reembolso_total) * 100, 1)
  ) %>%
  arrange(desc(num_devoluciones))

analisis_motivos

# Visualización
ggplot(analisis_motivos, aes(x = reorder(motivo, num_devoluciones), 
                              y = num_devoluciones)) +
  geom_col(fill = "steelblue") +
  coord_flip() +
  labs(title = "Devoluciones por Motivo",
       x = "Motivo", y = "Número de Devoluciones") +
  theme_minimal()

Caso de Estudio Completo (cont.)

Paso 3: Productos Problemáticos

# Unir devoluciones con ventas y productos
productos_devueltos <- devoluciones %>%
  left_join(ventas %>% select(id_venta, id_producto, id_tienda), 
            by = "id_venta") %>%
  left_join(productos %>% select(id_producto, nombre_producto, id_categoria),
            by = "id_producto") %>%
  left_join(categorias %>% select(id_categoria, nombre_categoria),
            by = "id_categoria")

# Top productos con más devoluciones
productos_problema <- productos_devueltos %>%
  group_by(id_producto, nombre_producto, nombre_categoria) %>%
  summarize(
    num_devoluciones = n(),
    reembolso_total = sum(reembolso),
    .groups = "drop"
  ) %>%
  arrange(desc(num_devoluciones)) %>%
  head(20)

productos_problema

# Calcular tasa de devolución por producto
tasa_devolucion_producto <- ventas %>%
  group_by(id_producto) %>%
  summarize(
    veces_vendido = n(),
    .groups = "drop"
  ) %>%
  left_join(
    devoluciones %>%
      left_join(ventas %>% select(id_venta, id_producto), by = "id_venta") %>%
      group_by(id_producto) %>%
      summarize(veces_devuelto = n(), .groups = "drop"),
    by = "id_producto"
  ) %>%
  mutate(
    veces_devuelto = replace_na(veces_devuelto, 0),
    tasa_devolucion = round(veces_devuelto / veces_vendido * 100, 2)
  ) %>%
  filter(veces_vendido >= 10) %>%  # solo productos con suficientes ventas
  arrange(desc(tasa_devolucion))

head(tasa_devolucion_producto, 10)

Paso 4: Análisis por Tienda y Categoría

# Devoluciones por tienda
devoluciones_tienda <- productos_devueltos %>%
  left_join(tiendas %>% select(id_tienda, nombre_tienda, region),
            by = "id_tienda") %>%
  group_by(id_tienda, nombre_tienda, region) %>%
  summarize(
    num_devoluciones = n(),
    reembolso_total = sum(reembolso),
    .groups = "drop"
  )

# Calcular tasa de devolución por tienda
ventas_por_tienda <- ventas %>%
  group_by(id_tienda) %>%
  summarize(num_ventas = n(), .groups = "drop")

comparacion_tiendas <- ventas_por_tienda %>%
  left_join(devoluciones_tienda, by = "id_tienda") %>%
  mutate(
    num_devoluciones = replace_na(num_devoluciones, 0),
    tasa_devolucion = round(num_devoluciones / num_ventas * 100, 2)
  ) %>%
  left_join(tiendas %>% select(id_tienda, nombre_tienda, region),
            by = "id_tienda") %>%
  arrange(desc(tasa_devolucion))

comparacion_tiendas

# Por categoría
devoluciones_categoria <- productos_devueltos %>%
  group_by(nombre_categoria) %>%
  summarize(
    num_devoluciones = n(),
    reembolso_total = sum(reembolso),
    motivo_principal = names(sort(table(motivo), decreasing = TRUE))[1],
    .groups = "drop"
  ) %>%
  arrange(desc(num_devoluciones))

devoluciones_categoria

Caso de Estudio Completo (y 3)

Paso 5: Impacto Financiero

# Impacto total
impacto_financiero <- ventas %>%
  summarize(
    ingresos_brutos = sum(total),
    .groups = "drop"
  ) %>%
  mutate(
    reembolsos = sum(devoluciones$reembolso),
    ingresos_netos = ingresos_brutos - reembolsos,
    tasa_devolucion_financiera = round(reembolsos / ingresos_brutos * 100, 2)
  )

impacto_financiero

# Impacto por período
impacto_temporal <- ventas %>%
  mutate(año_mes = paste(año, sprintf("%02d", mes), sep = "-")) %>%
  group_by(año_mes) %>%
  summarize(
    ingresos_brutos = sum(total),
    .groups = "drop"
  ) %>%
  left_join(
    devoluciones %>%
      left_join(ventas %>% select(id_venta, año, mes), by = "id_venta") %>%
      mutate(año_mes = paste(año, sprintf("%02d", mes), sep = "-")) %>%
      group_by(año_mes) %>%
      summarize(reembolsos = sum(reembolso), .groups = "drop"),
    by = "año_mes"
  ) %>%
  mutate(
    reembolsos = replace_na(reembolsos, 0),
    ingresos_netos = ingresos_brutos - reembolsos,
    tasa_devolucion = round(reembolsos / ingresos_brutos * 100, 2)
  ) %>%
  arrange(año_mes)

# Visualizar tendencia
ggplot(impacto_temporal, aes(x = año_mes, y = tasa_devolucion, group = 1)) +
  geom_line(color = "red", size = 1) +
  geom_point() +
  theme_minimal() +
  theme(axis.text.x = element_text(angle = 45, hjust = 1)) +
  labs(title = "Evolución de la Tasa de Devolución",
       x = "Período", y = "Tasa de Devolución (%)")

Caso de Estudio Completo (y 4)

Paso 6: Recomendaciones Accionables

# Crear informe ejecutivo
informe_devoluciones <- list(
  resumen = devoluciones %>%
    summarize(
      total_devoluciones = n(),
      tasa_global = round(n() / nrow(ventas) * 100, 2),
      costo_total = sum(reembolso)
    ),
  
  productos_criticos = tasa_devolucion_producto %>%
    filter(tasa_devolucion > 10) %>%
    head(10),
  
  tiendas_problema = comparacion_tiendas %>%
    filter(tasa_devolucion > 7) %>%
    head(5),
  
  categorias_riesgo = devoluciones_categoria %>%
    head(3)
)

# Mostrar informe
informe_devoluciones

Recomendaciones:

  1. Productos: Revisar calidad de productos con tasa >10%
  2. Tiendas: Capacitación en tiendas con alta tasa de devolución
  3. Categorías: Mejorar descripción de productos en categorías problemáticas
  4. Procesos: Implementar mejor política de cambios para “Talla incorrecta”

Buenas Prácticas y Errores Comunes

Trabajar con Valores Ausentes (NA)

# Identificar NAs
ventas %>%
  summarize(
    na_cliente = sum(is.na(id_cliente)),
    na_producto = sum(is.na(id_producto)),
    na_total = sum(is.na(total))
  )

# Filtrar NAs
ventas_sin_na <- ventas %>%
  filter(!is.na(id_cliente))

# Eliminar filas con ANY NA
ventas_completas <- ventas %>%
  drop_na()

# Eliminar NAs de columnas específicas
ventas %>%
  drop_na(id_cliente, total)

# Reemplazar NAs
clientes_limpio <- clientes %>%
  mutate(
    programa_fidelidad = replace_na(programa_fidelidad, "Básico")
  )

# NA en operaciones: usar na.rm = TRUE
ventas %>%
  summarize(
    media_con_na = mean(descuento_porcentaje),
    media_sin_na = mean(descuento_porcentaje, na.rm = TRUE)
  )

Errores Comunes y Soluciones

Error 1: Olvidar ungroup()

# PROBLEMA
datos_agrupados <- ventas %>%
  group_by(id_tienda) %>%
  mutate(total_tienda = sum(total))

# Operaciones siguientes están agrupadas (puede causar errores)
datos_agrupados %>%
  summarize(media_global = mean(total))  # No es media global!

# SOLUCIÓN
datos_correctos <- ventas %>%
  group_by(id_tienda) %>%
  mutate(total_tienda = sum(total)) %>%
  ungroup() %>%
  summarize(media_global = mean(total))

Errores Comunes y Soluciones (cont.)

Error 2: Nombres de columnas con espacios o caracteres especiales

# PROBLEMA
datos_problema <- tibble(
  `Nombre Producto` = c("A", "B", "C"),
  `Precio (€)` = c(10, 20, 30)
)

# Usar backticks
datos_problema %>%
  select(`Nombre Producto`, `Precio (€)`)

# MEJOR: Usar nombres sin espacios
datos_bien <- datos_problema %>%
  rename(
    nombre_producto = `Nombre Producto`,
    precio_euros = `Precio (€)`
  )

Errores Comunes y Soluciones (y 3)

Error 3: No verificar claves duplicadas en joins

# PROBLEMA: claves duplicadas pueden multiplicar filas
df1 <- tibble(id = c(1, 1, 2), valor = c("A", "B", "C"))
df2 <- tibble(id = c(1, 2), info = c("X", "Y"))

resultado <- left_join(df1, df2, by = "id")
nrow(resultado)  # Esperamos 3, tenemos 3 (pero revisar lógica)

# VERIFICAR antes de hacer join
df1 %>% count(id) %>% filter(n > 1)  # ids duplicados
df2 %>% count(id) %>% filter(n > 1)  # ids duplicados

# SOLUCIÓN: decidir qué hacer con duplicados
df1_unico <- df1 %>% distinct(id, .keep_all = TRUE)

Error 4: Orden de operaciones incorrecto

# PROBLEMA: filtrar después de summarize
ventas %>%
  group_by(id_tienda) %>%
  summarize(total = sum(total)) %>%
  filter(cantidad > 3)  # ERROR: cantidad ya no existe

# SOLUCIÓN: filtrar antes de summarize
ventas %>%
  filter(cantidad > 3) %>%
  group_by(id_tienda) %>%
  summarize(total = sum(total))

Optimización

# USO EFICIENTE: filtrar primero, luego unir
ventas_2023 <- ventas %>%
  filter(año == 2023) %>%  # reduce tamaño
  left_join(productos, by = "id_producto")

# USO INEFICIENTE: unir todo, luego filtrar
ventas_2023_lento <- ventas %>%
  left_join(productos, by = "id_producto") %>%
  filter(año == 2023)

# SELECCIONAR SOLO COLUMNAS NECESARIAS
productos_minimo <- productos %>%
  select(id_producto, nombre_producto, precio)

ventas_join <- ventas %>%
  left_join(productos_minimo, by = "id_producto")

Gráficos de Análisis Exploratorio

# Distribución de ventas
ggplot(ventas, aes(x = total)) +
  geom_histogram(bins = 50, fill = "steelblue", alpha = 0.7) +
  labs(title = "Distribución de Ventas",
       x = "Importe Total (€)", y = "Frecuencia") +
  theme_minimal()
# Ventas por día de la semana
ventas_dia <- ventas %>%
  group_by(dia_semana) %>%
  summarize(
    num_ventas = n(),
    ingresos = sum(total),
    ticket_promedio = mean(total)
  )

ggplot(ventas_dia, aes(x = dia_semana, y = ingresos)) +
  geom_col(fill = "coral") +
  labs(title = "Ingresos por Día de la Semana",
       x = "Día", y = "Ingresos (€)") +
  theme_minimal()
# Evolución temporal
ventas_mensual <- ventas %>%
  group_by(año, mes) %>%
  summarize(ingresos = sum(total), .groups = "drop") %>%
  mutate(periodo = paste(año, sprintf("%02d", mes), sep = "-"))

ggplot(ventas_mensual, aes(x = periodo, y = ingresos, group = 1)) +
  geom_line(color = "darkgreen", size = 1) +
  geom_point() +
  theme_minimal() +
  theme(axis.text.x = element_text(angle = 45, hjust = 1)) +
  labs(title = "Evolución de Ingresos Mensuales",
       x = "Período", y = "Ingresos (€)")

Comparaciones y Rankings

# Top 10 tiendas por ingresos
top_tiendas <- ventas %>%
  left_join(tiendas %>% select(id_tienda, nombre_tienda), by = "id_tienda") %>%
  group_by(nombre_tienda) %>%
  summarize(ingresos = sum(total), .groups = "drop") %>%
  arrange(desc(ingresos)) %>%
  head(10)

ggplot(top_tiendas, aes(x = reorder(nombre_tienda, ingresos), y = ingresos)) +
  geom_col(fill = "steelblue") +
  coord_flip() +
  labs(title = "Top 10 Tiendas por Ingresos",
       x = "Tienda", y = "Ingresos (€)") +
  theme_minimal()
# Comparación entre regiones
ventas_region <- ventas %>%
  left_join(tiendas %>% select(id_tienda, region), by = "id_tienda") %>%
  group_by(region, año) %>%
  summarize(ingresos = sum(total), .groups = "drop")

ggplot(ventas_region, aes(x = factor(año), y = ingresos, fill = region)) +
  geom_col(position = "dodge") +
  labs(title = "Ingresos por Región y Año",
       x = "Año", y = "Ingresos (€)", fill = "Región") +
  theme_minimal()

Guardar Datasets Procesados

# Guardar en RData
save(ventas_completo, file = "data/ventas_procesado.RData")

# Guardar en CSV
write_csv(ventas_completo, "data/ventas_procesado.csv")

# Guardar múltiples objetos
save(analisis_rentabilidad, comparacion_objetivos, 
     file = "data/analisis_completo.RData")

Crear Reportes

# Tabla resumen para Excel
reporte_ejecutivo <- ventas %>%
  left_join(tiendas %>% select(id_tienda, nombre_tienda, region), 
            by = "id_tienda") %>%
  group_by(region, año, trimestre) %>%
  summarize(
    num_transacciones = n(),
    ingresos = sum(total),
    ticket_promedio = mean(total),
    clientes_unicos = n_distinct(id_cliente),
    .groups = "drop"
  ) %>%
  pivot_wider(
    names_from = trimestre,
    values_from = c(ingresos, num_transacciones),
    names_glue = "{.value}_Q{trimestre}"
  )

# Exportar
library(writexl)
write_xlsx(reporte_ejecutivo, "reportes/reporte_ejecutivo.xlsx")

Ejercicios de Práctica

Ejercicio 1: Análisis de Categorías

Usando los datos de RetailCorp:

  1. Identifica las 3 categorías con mayores ingresos en 2023
  2. Calcula la tasa de crecimiento año sobre año para cada categoría
  3. Determina qué categorías tienen los tickets promedio más altos
  4. Crea un gráfico comparativo

Ejercicio 2: Segmentación de Clientes

  1. Clasifica clientes según su valor total de compras (Bajo/Medio/Alto/Premium)
  2. Calcula la recencia (días desde última compra)
  3. Identifica clientes en riesgo (no han comprado en >90 días)
  4. Propón estrategia de retención

Ejercicio 3: Análisis de Empleados

  1. Calcula el promedio de ventas por empleado
  2. Identifica empleados top performers
  3. Analiza si hay diferencias por puesto
  4. Sugiere un sistema de bonificaciones

Ejercicio 4: Optimización de Inventario

  1. Lista productos con baja rotación (pocas ventas)
  2. Identifica productos descontinuados
  3. Calcula el stock óptimo por tienda (simulado)
  4. Recomienda productos a eliminar del catálogo